Skip to content

New API to observe user input #590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 14, 2025
Merged

New API to observe user input #590

merged 22 commits into from
May 14, 2025

Conversation

mickael-menu
Copy link
Member

Changelog

Added

Navigator

  • A new InputObserving API has been added to enable more flexible gesture recognition and support for mouse pointers.

User guide

In visual publications like EPUB or PDF, users can interact with the VisualNavigator instance using gestures, a keyboard, a mouse, a pencil, or a trackpad. When the publication does not override user input events, you may want to intercept them to trigger actions in your user interface. For example, you can turn pages or toggle the navigation bar on taps, or open a bookmarks screen when pressing the Command-Shift-D hotkey.

VisualNavigator implements InputObservable, providing a way to intercept input events.

Implementing an input observer

Here's an example of a simple InputObserving implementation that logs all key and pointer events emitted by the navigator.

navigator.addObserver(InputObserver())

@MainActor final class InputObserver: InputObserving {
    func didReceive(_ event: PointerEvent) -> Bool {
        print("Received pointer event: \(event)")
        return false
    }

    func didReceive(_ event: KeyEvent) -> Bool {
        print("Received key event: \(event)")
        return false
    }
}

If you choose to handle a specific event, return true to prevent subsequent observers from using it. This is useful when you want to avoid triggering multiple actions upon receiving an event.

An InputObserving implementation receives low-level events that can be used to create higher-level gesture recognizers, such as taps or pinches. To assist you, the toolkit also offers helpers to observe tap, click and key press events.

Observing tap and click events

The ActivatePointerObserver is an implementation of InputObserving recognizing single taps or clicks. You can use the convenient static factories to observe these events.

navigator.addObserver(.tap { event in
    print("User tapped at \(event.location)")
    return false
})

// Key modifiers can be used to recognize an event only when a modifier key is pressed.
navigator.addObserver(.click(modifiers: [.shift]) { event in
    print("User clicked at \(event.location)")
    return false
})

Observing keyboard events

Similarly, the KeyboardObserver implementation provides an easy method to intercept keyboard events.

navigator.addObserver(.key { event in
    print("User pressed the key \(event.key) with modifiers \(event.modifiers)")
    return false
})

It can also be used to observe a specific keyboard shortcut.

navigator.addObserver(.key(.a) {
    self.log(.info, "User pressed A")
    return false
})

navigator.addObserver(.key(.a, [.control, .shift]) {
    self.log(.info, "User pressed Control+Shift+A")
    return false
})

Migration guide

The DirectionalNavigationAdapter was also updated to use this new API, and is easier to use.

To migrate the old API, remove the old VisualNavigatorDelegate callbacks, for example:

-func navigator(_ navigator: VisualNavigator, didTapAt point: CGPoint) {
-    Task {
-        // Turn pages when tapping the edge of the screen.
-        guard await !DirectionalNavigationAdapter(navigator: navigator).didTap(at: point) else {
-            return
-        }
-        // clear a current search highlight
-        if let decorator = self.navigator as? DecorableNavigator {
-            decorator.apply(decorations: [], in: "search")
-        }
-
-        toggleNavigationBar()
-    }
-}
-
-func navigator(_ navigator: VisualNavigator, didPressKey event: KeyEvent) {
-    Task {
-        // Turn pages when pressing the arrow keys.
-        await DirectionalNavigationAdapter(navigator: navigator).didPressKey(event: event)
-    }
-}

Instead, setup your observers after initializing the navigator. Return true if you want to stop the propagation of a particular event to the next observers.

/// This adapter will automatically turn pages when the user taps the
/// screen edges or press arrow keys.
///
/// Bind it to the navigator before adding your own observers to prevent
/// triggering your actions when turning pages.
DirectionalNavigationAdapter().bind(to: navigator)

// Clear the current search highlight on tap.
navigator.addObserver(.tap { [weak self] _ in
    guard
        let searchViewModel = self?.searchViewModel,
        searchViewModel.selectedLocator != nil
    else {
        return false
    }

    searchViewModel.selectedLocator = nil
    return true
})

// Toggle the navigation bar on tap, if nothing else took precedence.
navigator.addObserver(.tap { [weak self] _ in
    self?.toggleNavigationBar()
    return true
})

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new InputObserving API to capture and forward user input events in a more flexible and unified way across touch, pointer, and keyboard interactions. Key changes include the implementation of new protocols (InputObserving, InputObservable) and adapters (InputObservingGestureRecognizerAdapter), refactoring of legacy input callbacks in navigator view controllers, and corresponding updates in related JavaScript files for key and pointer events.

Reviewed Changes

Copilot reviewed 40 out of 41 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Sources/Navigator/Input/Key/Key.swift Added Key enum with support for various key types and helper properties.
Sources/Navigator/Input/InputObserving*.swift Introduced new interfaces and implementations for observing input events.
Sources/Navigator/EPUB/* Updated EPUB view controllers to utilize the new InputObserving API and removed legacy tap/press handlers.
Sources/Navigator/DirectionalNavigationAdapter.swift Migrated to use the new bind(to:) pattern, deprecating old methods.
Sources/Navigator/EPUB/Scripts/src/*.js Replaced deprecated key event messaging with sendKeyEvent calls and added pointer event listeners.
Others Package and changelog updates reflecting dependency bumps and the new API introduction.
Files not reviewed (1)
  • Sources/Navigator/EPUB/Scripts/pnpm-lock.yaml: Language not supported

@mickael-menu mickael-menu merged commit c8fdd9f into develop May 14, 2025
5 checks passed
@mickael-menu mickael-menu deleted the feature/pointer branch May 14, 2025 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant